---
title: デバウンス/キャンセルによるネットワークリクエストの管理
version: 1
---

import { Link } from "/src/components/Link";
import { AutoSnippet, When } from "/src/components/CodeSnippet";
import homeScreen from "!raw-loader!/docs/case_studies/cancel/home_screen.dart";
import extension from "!raw-loader!/docs/case_studies/cancel/extension.dart";
import detailScreen from "/docs/case_studies/cancel/detail_screen";
import detailScreenCancel from "/docs/case_studies/cancel/detail_screen_cancel";
import detailScreenDebounce from "/docs/case_studies/cancel/detail_screen_debounce";
import providerWithExtension from "/docs/case_studies/cancel/provider_with_extension";

アプリケーションが複雑になると、同時に複数のネットワークリクエストが発生することが一般的です。  
例えば、ユーザーが検索ボックスに入力するたびに新しいリクエストがトリガーされる場合があります。  
ユーザーが速く入力する場合、アプリケーションは同時に多くのリクエストを処理することになります。

あるいは、ユーザーがリクエストをトリガーした後に別のページに移動する場合もあります。  
この場合、アプリケーションには不要になったリクエストが残る可能性があります。

これらの状況でパフォーマンスを最適化するためには、いくつかのテクニックを使用できます:

- リクエストの"デバウンス"。これはユーザーが一定時間入力を停止するまでリクエストを送信しないようにする方法です。  
  これにより、ユーザーが速く入力しても、特定の入力に対して 1 回のリクエストしか送信しないようになります。
- リクエストの"キャンセル"。これは、リクエストが完了する前にユーザーがページから移動するとリクエストをキャンセルする方法です。  
  これにより、ユーザーが見ることのないレスポンスを処理する無駄を省けます。

Riverpod では、これらの 2 つのテクニックを似たような方法で実装できます。  
重要なのは`ref.onDispose`を使用して"自動破棄"と組みわせるか、`ref.watch`を使用して望ましい動作を実現することです。

これを示すために、2 つのページからなるシンプルなアプリケーションを作成します:

- ホーム画面：ボタンを押すと新しいページが開きます
- 詳細ページ：[Bored API](https://www.boredapi.com/) からランダムなアクティビティを表示し、アクティビティをリフレッシュできます。
  pull-to-refresh の実装方法については<Link documentID="case_studies/pull_to_refresh" />をご覧ください。

次に、以下の動作を実装します:

- ユーザーが詳細ページを開いてすぐに戻った場合、アクティビティのリクエストをキャンセルします。
- ユーザーが連続してアクティビティをリフレッシュする場合、リクエストをデバウンスしてユーザーがリフレッシュを停止してから 1 つのリクエストを送信します。

## アプリケーション

<img
  src="/img/case_studies/cancel/app.gif"
  alt="アプリケーションを紹介し、詳細ページを開き、アクティビティをリフレッシュするGIF。"
/>

まず最初に、デバウンスやキャンセルなしでアプリケーションを作成しましょう。  
ここでは特に特別なことはせず、`FloatingActionButton`と`Navigator.push`を使って詳細ページを開きます。

まず、ホーム画面を定義しましょう。  
通常通り、アプリケーションのルートに`ProviderScope`を指定することを忘れないでください。

<AutoSnippet title="lib/src/main.dart" raw={homeScreen} translations={{}} />

次に、詳細ページを定義しましょう。  
アクティビティを取得し、pull-to-refresh を実装するには、<Link documentID="case_studies/pull_to_refresh" />を参照してください。

<AutoSnippet
  title="lib/src/detail_screen.dart"
  {...detailScreen}
  translations={{}}
/>

## リクエストのキャンセル

アプリケーションが動作するようになったので、キャンセルロジックを実装しましょう。

これを行うために、`ref.onDispose`を使用してユーザーがページから離れた時にリクエストをキャンセルします。  
この機能を利用するには、provider の自動破棄が有効であることが重要です。

リクエストをキャンセルするために必要な正確なコードは HTTP クライアントによって異なります。  
この例では`package:http`を使用しますが、他のクライアントでも同じ原則が適用されます。

ここでの鍵は、ユーザーが別の場所に移動すると`ref.onDispose`が呼び出されることです。  
これは provider が使用されなくなり、自動破棄によって破棄されるためです。  
そのため、このコールバックを使用してリクエストをキャンセルできます。  
`package:http`を使用する場合、HTTP クライアントを閉じることでこれを実現できます。

<AutoSnippet
  {...detailScreenCancel}
  translations={{
    client: "  // package:httpを使用して HTTP クライアントを作成します",
    onDispose:
      "  // onDispose時には、クライアントを閉じます。\n  // クライアントが持っているかもしれない保留中のリクエストはすべてキャンセルされます。",
    get: '  // ここでは、"get"関数の代わりにクライアントを使ってリクエストを行う。',
    jsonDecode: "  // 残りのコードは以前と同じです。",
  }}
/>

## リクエストのデバウンス

キャンセルを実装したので、デバウンスを実装しましょう。  
現時点では、ユーザーがアクティビティを連続してリフレッシュすると、リフレッシュごとにリクエストが送信されます。

技術的には、キャンセルを実装したのでこれは問題ではありません。  
ユーザーが連続してアクティビティをリフレッシュすると、新しいリクエストが行われるたびに前のリクエストがキャンセルされます。

しかし、これは理想的ではありません。  
複数のリクエストを送信し、帯域幅とサーバーリソースを無駄にすることになります。  
そのため、ユーザーが一定時間アクティビティをリフレッシュするまでリクエストを遅延させることができます。

ロジックはキャンセルロジックと非常に似ています。再び`ref.onDispose`を使用します。  
しかし、ここでは HTTP クライアントを閉じる代わりに、リクエストが開始される前に `onDispose` を使用してリクエストを中止します。  
その後、リクエストを送信する前に任意の 500ms を待機します。
次に、500ms が経過する前にユーザーが再びアクティビティをリフレッシュすると、`onDispose`が呼び出され、リクエストが中止されます。

:::info
リクエストを中止するために、意図的に例外をスローすることが一般的です。  
provider が破棄された後に provider 内でスローするのは安全です。  
例外は自然に Riverpod によってキャッチされ、無視されます。
:::

<AutoSnippet
  {...detailScreenDebounce}
  translations={{
    didDispose: "  // providerが現在破棄中かどうかをキャプチャします。",
    delayed:
      "  // ユーザーがリフレッシュを停止するのを待つために、リクエストを500ms遅延させます。",
    cancelled:
      "  // 遅延中にproviderが破棄された場合、ユーザーが再度リフレッシュしたことを意味します。\n  // リクエストをキャンセルするために例外をスローします。\n  // Riverpodがキャッチするので、ここで例外を使用することは安全です。",
    http: "  // 以下のコードは前のスニペットから変更されていません。",
  }}
/>

## 両方を同時に実行する: デバウンスとキャンセル

デバウンスとキャンセルのリクエストの方法を理解りました。  
しかし、別のリクエストを行うには、同じロジックを複数の場所にコピーペーストして貼り付ける必要があります。  
これは理想的ではありません。

そこで、デバウンスとキャンセルの両方を同時に処理する再利用可能なユーティリティを実装することができます。

ここでのアイデアは、`Ref`に対して拡張メソッドを実装し、キャンセルとデバウンスの両方を 1 つのメソッドで処理することです。

<AutoSnippet
  raw={extension}
  translations={{
    note: "  /// [duration]（デフォルトは500ms）待機し、その後リクエストを行うために使用できる[http.Client]を返します。\n  ///\n  /// そのクライアントはproviderが破棄されたときに自動的に閉じられます。",
    didDispose: "    // まず、デバウンスを処理します。",
    delay:
      "    // ユーザーがリフレッシュを停止するのを待つために、リクエストを500ms遅延させます。",
    cancel:
      "    // 遅延中にproviderが破棄された場合、ユーザーが再度リフレッシュしたことを意味します。\n    // リクエストをキャンセルするために例外をスローします。\n    // Riverpodがキャッチするので、ここで例外を使用することは安全です。",
    client: "    // クライアントを作成し、providerが破棄されたときに閉じます。",
    return:
      "    // 最後に、providerがリクエストを行うためにクライアントを返します。",
  }}
/>

この拡張メソッドを provider で以下のように使用できます:

<AutoSnippet
  {...providerWithExtension}
  translations={{
    client:
      "  // 先ほど作成した拡張機能を使用してHTTPクライアントを取得します。",
    get: '  // クライアントを使用してリクエストを行います。"get"関数の代わりに使用します。\n  // リクエストは自然にデバウンスされ、ユーザーがページを離れた場合はキャンセルされます。',
  }}
/>
